﻿using System;
using System.Collections.Generic;
using BFlatCompile.Nodes;

namespace BFlatCompile.Serializer
{
	public static class ByteCodeExecSerializer
	{
		public static void Serialize(Compiler c, Buffer b, Executable ex)
		{
			// Forgive me father for I am about to sin.

			// The reason I don't put these methods on the nodes themselves is because
			// I'd like to keep the Node namespace 100% free of any serializer logic.

			if (ex is Return) SerializeReturn(c, b, (Return)ex);
			else if (ex is ExpressionAsExecutable) SerializeExpressionAsExecutable(c, b, (ExpressionAsExecutable)ex);
			else if (ex is Switch) SerializeSwitch(c, b, (Switch)ex);
			else if (ex is Break) SerializeBreak(c, b, (Break)ex);
			else if (ex is Loop) SerializeLoop(c, b, (Loop)ex);
			else if (ex is IfStatement) SerializeIfStatement(c, b, (IfStatement)ex);
			else if (ex is Assignment) SerializeAssignment(c, b, (Assignment)ex);
			else if (ex is TryBlock) SerializeTryBlock(c, b, (TryBlock)ex);
			else if (ex is ApplyParam) SerializeApplyParam(c, b, (ApplyParam)ex);
			else if (ex is Throw) SerializeThrow(c, b, (Throw)ex);
			else if (ex is ArgMatch) SerializeArgMatch(c, b, (ArgMatch)ex);
			else if (ex is ClassInitFinalizer) SerializeClassInitFinalizer(c, b, (ClassInitFinalizer)ex);
			else if (ex is InvokeStaticConstructor) SerializeInvokeStaticConstructor(c, b, (InvokeStaticConstructor)ex);
			else
			{
				throw new Exception("This shouldn't happen.");
			}
		}

		private static void SerializeInvokeStaticConstructor(Compiler c, Buffer b, InvokeStaticConstructor exec)
		{
			b.Add(ByteCodes.INVOKE_STATIC_CONSTRUCTOR(c, exec.ClassID));
		}

		private static void SerializeClassInitFinalizer(Compiler c, Buffer b, ClassInitFinalizer exec)
		{
			b.Add(ByteCodes.CLASS_INIT_COMPLETE(c, exec.ID));
		}

		private const int CATEGORY_ANYTHING = 0;
		private const int CATEGORY_NULL = 1;
		private const int CATEGORY_INT = 2;
		private const int CATEGORY_FLOAT = 3;
		private const int CATEGORY_NUM = 4;
		private const int CATEGORY_BOOL = 5;
		private const int CATEGORY_STRING = 6;
		private const int CATEGORY_LIST = 7;
		private const int CATEGORY_INSTANCE = 8;

		private static void SerializeArgMatch(Compiler c, Buffer b, ArgMatch exec)
		{
			/*
			 * if no else is present:
			 * [arg check and jump if false]
			 * [trueCode]
			 * [other code]
			 * 
			 * else:
			 * [arg check and jump if false]
			 * [trueCode + unconditional jump]
			 * [else code]
			 * [other code]
			 */

			Buffer trueCode = new Buffer();

			SerializeLines(c, trueCode, exec.TrueCode);

			Buffer falseCode = null;
			if (exec.FalseCode != null && exec.FalseCode.Length > 0)
			{
				falseCode = new Buffer();
				SerializeLines(c, falseCode, exec.FalseCode);
				trueCode.Add(ByteCodes.JUMP(c, falseCode.Size));
			}

			int jumpSize = trueCode.Size;

			List<int> argCheck = new List<int>() { jumpSize };
			List<int> types = new List<int>();
			int matchCount = 0;
			int typeValue;
			foreach (string match in exec.MatchExpressions)
			{
				if (match.StartsWith("INSTANCE:"))
				{
					string className = match.Substring("INSTANCE:".Length);
					int classId = 0; 
					if (className != "ANY")
					{
						// TODO: convert className into a class ID
					}
					types.Add(CATEGORY_INSTANCE);
					types.Add(classId);
				}
				else
				{
					switch (match)
					{
						case "ANY":
						case "ANYTHING": typeValue = CATEGORY_ANYTHING; break;
						case "NUM": typeValue = CATEGORY_NUM; break;
						case "INTEGER":
						case "INT": typeValue = CATEGORY_INT; break;
						case "FLOAT": typeValue = CATEGORY_FLOAT; break;
						case "BOOLEAN":
						case "BOOL": typeValue = CATEGORY_BOOL; break;
						case "NULL": typeValue = CATEGORY_NULL; break;
						case "STRING": typeValue = CATEGORY_STRING; break;
						case "LIST": typeValue = CATEGORY_LIST; break;
						default:
							throw new Exception("Invalid arg matcher value: " + match);
					}

					types.Add(typeValue);
				}
				++matchCount;
			}

			argCheck.Add(matchCount);
			argCheck.AddRange(types);
			b.Add(ByteCodes.ARG_CHECK(c, argCheck.ToArray()));
			b.Add(trueCode);
			if (falseCode != null)
			{
				b.Add(falseCode);
			}
		}

		private static void SerializeThrow(Compiler c, Buffer b, Throw exec)
		{
			if (exec.Expression == null)
			{
				b.Add(ByteCodes.THROW_BUBBLING(c), exec.Token);
			}
			else
			{
				ByteCodeExprSerializer.Serialize(c, b, exec.Expression);
				b.Add(ByteCodes.THROW(c), exec.Token);
			}
		}

		private static void SerializeApplyParam(Compiler c, Buffer b, ApplyParam exec)
		{
			MethodParam p = exec.Param;
			int index = exec.Index;
			int defaultParamId = 0;
			if (exec.Param.Value != null)
			{
				defaultParamId = exec.Param.ID;
			}

			Variable target = new Variable(p.Name, exec.Token);
			ByteCodeExprSerializer.SerializeVariable(c, b, target, true);
			bool argVerificationAllowed = exec.Token.RelativeFile.StartsWith("$");

			// TODO: reconsider why this is else was here.
			// APPLY_ARG_N ought to either apply the passed arg or make the proper function call to the default value generator
			// (and eventually just grab the constant from a lookup table if the arg default value is an immutable constant, that's also a TODO)

			//if (p.Value == null)
			//{
				b.Add(ByteCodes.APPLY_ARG_N(c, index, defaultParamId));
				if (argVerificationAllowed)
				{
					b.Add(ByteCodes.ARG_PRE_VERIFY(c));
				}
			//}
			/*
			else
			{
				Buffer tb = new Buffer();
				ByteCodeExprSerializer.Serialize(c, tb, p.Value);
				b.Add(ByteCodes.JUMP_IF_NO_ARG_PRESENT(c, index, 2));
				b.Add(ByteCodes.APPLY_ARG_N(c, index, defaultParamId));
				b.Add(ByteCodes.JUMP(c, tb.Size));
				b.Add(tb);
			}
			//*/

			b.Add(ByteCodes.ASSIGN(c));
		}

		private static void SerializeTryBlock(Compiler c, Buffer b, TryBlock exec)
		{
			/*
			 * [TRY_PUSH $catch_loc $finally_loc]
			 * [ ... try code ... ]
			 * [JUMP $finally_loc]
			 * [VAR ex] [active exception] [assign]
			 * [ ... catch code ... ]
			 * finally loc: [ ... finally code ... ]
			 * [TRY_POP]
			 * 
			 * TRY_PUSH
			 *    Save try-catch information onto the stack. This includes the catch location 
			 *    and the finally location. The catch location begins with an assignment to the
			 *    exception variable, if present. 
			 *    
			 *    IMPORTANT:
			 *      TRY_PUSH params are OFFSETS but are resolved into ABSOLUTE ADDRESSES when 
			 *      the byte code is done being compiled and the final length is known.
			 *    
			 *    If an exception is thrown or bubbled at any point:
			 *      - if the try stack is empty
			 *        - bubble it
			 *      - else:
			 *        - set that exception to the active exception on top of the try-stack.
			 *        - if a catch block is present and you are before it:
			 *          - mark the exception as handled
			 *          - jump to the catch block
			 *        - else if a finally block is present and you are before it:
			 *          - do not mark the exception as handled
			 *          - jump to the finally block
			 * 
			 *    If a value is returned at any point:
			 *      - if the try stack is empty
			 *        - return it
			 *      - else:
			 *        - if the finally block is not present:
			 *          - return it
			 *        - if you are before the finally block:
			 *          - set the return value of the top of the try stack to the value
			 *          - jump to the try stack
			 *        - else:
			 *          - return it
			 * 
			 * TRY_POP
			 *    if there is an active unhandled exception:
			 *      bubble it
			 *    else if there is a returned value:
			 *      return it
			 *    else:
			 *      Pop the try stack
			 * 
			 * Try without either catch or finally is invalid and won't compile.
			 */

			bool catchExists = exec.CatchCode != null;
			bool finallyExists = exec.FinallyCode != null && exec.FinallyCode.Length > 0;

			Buffer tryCode = new Buffer();
			Buffer catchCode = null;
			Buffer finallyCode = null;

			SerializeLines(c, tryCode, exec.TryCode);

			if (tryCode.Size == 0)
			{
				tryCode.Add(ByteCodes.NOOP(c)); // to make offset not 0 which is code for "does not exist"
			}

			if (catchExists)
			{
				catchCode = new Buffer();

				if (exec.ExceptionName != null)
				{
					// TODO: you should create an actual variable instance for this, so that it will get 
					// hit by the resolvers.
					// Also, it'll be easier to streamline verification that they don't use a stupid name like "class" or "this"
					// in the resolver phase. Hmm...I need to do that.
					catchCode.Add(ByteCodes.VARIABLE_WRAPPED(c, exec.ExceptionName.Index));
					catchCode.Add(ByteCodes.ACTIVE_EXCEPTION(c));
					catchCode.Add(ByteCodes.ASSIGN(c));
				}

				if (exec.CatchCode.Length > 0)
				{
					SerializeLines(c, catchCode, exec.CatchCode);
				}
				else
				{
					catchCode.Add(ByteCodes.NOOP(c));
				}
			}

			int catchLocation = 0;
			int finallyLocation = 0;

			if (finallyExists)
			{
				finallyCode = new Buffer();
				SerializeLines(c, finallyCode, exec.FinallyCode);
			}

			if (catchExists)
			{
				tryCode.Add(ByteCodes.JUMP(c, catchCode.Size));
				catchLocation = tryCode.Size + 1;
			}

			if (finallyExists)
			{
				finallyLocation = catchExists
					? (catchLocation + catchCode.Size)
					: (tryCode.Size + 1);
			}

			b.Add(ByteCodes.TRY_PUSH(c, catchLocation, finallyLocation));
			b.Add(tryCode);

			if (catchCode != null)
			{
				b.Add(catchCode);
			}

			if (finallyCode != null)
			{
				b.Add(finallyCode);
			}

			b.Add(ByteCodes.TRY_POP(c));
		}

		private static void SerializeAssignment(Compiler c, Buffer b, Assignment exec)
		{
			Expression target = exec.Target;

			if (target is Variable)
			{
				ByteCodeExprSerializer.SerializeVariable(c, b, (Variable)target, true);
			}
			else if (target is DotDereference)
			{
				ByteCodeExprSerializer.SerializeDotDereference(c, b, (DotDereference)target, true);
			}
			else if (target is BracketIndex)
			{
				ByteCodeExprSerializer.SerializeBracketIndex(c, b, (BracketIndex)target, true);
			}
			else
			{
				// TODO: verify this error gets thrown pre-emptively by the parser
				throw new Exception("Invalid target for assignment");
			}

			if (exec.Op != '=')
			{
				b.Add(ByteCodes.KEEP_AND_DEREF_POINTER(c), exec.Token);
			}

			ByteCodeExprSerializer.Serialize(c, b, exec.Amount);

			switch (exec.Op)
			{
				case '=': break;
				case '+': b.Add(ByteCodes.ADD(c), exec.Token); break;
				case '-': b.Add(ByteCodes.SUBTRACT(c), exec.Token); break;
				case '*': b.Add(ByteCodes.MULTIPLY(c), exec.Token); break;
				case '/': b.Add(ByteCodes.DIVIDE(c), exec.Token); break;
				case '%': b.Add(ByteCodes.MOD(c), exec.Token); break;
				case '&': b.Add(ByteCodes.BITWISE_AND(c), exec.Token); break;
				case '|': b.Add(ByteCodes.BITWISE_OR(c), exec.Token); break;
				case '^': b.Add(ByteCodes.BITWISE_XOR(c), exec.Token); break;
				default: throw new Exception("Unrecognized assignment operator");
			}

			b.Add(ByteCodes.ASSIGN(c), exec.Token);
		}

		public static void SerializeLines(Compiler c, Buffer b, Executable[] lines)
		{
			foreach (Executable line in lines)
			{
				Serialize(c, b, line);
			}
		}

		private static void SerializeIfStatement(Compiler c, Buffer b, IfStatement exec)
		{
			Expression condition = exec.Condition;
			ByteCodeExprSerializer.Serialize(c, b, condition);

			if (exec.FalseCode == null || exec.FalseCode.Length == 0)
			{
				// true-only
				Buffer trueBuffer = new Buffer();
				SerializeLines(c, trueBuffer, exec.TrueCode);

				b.Add(ByteCodes.JUMP_IF_FALSE(c, trueBuffer.Size), exec.OpenParenToken);
				b.Add(trueBuffer);
			}
			else
			{
				// both true and false
				// put a jump-if-true after the condition
				// place the true condition AFTER the false condition (one less bool flip in the interpreter)
				// at the end of the false buffer, add a jump to skip the true code (do this before calculating the jump offset for the condition)

				Buffer trueBuffer = new Buffer();
				SerializeLines(c, trueBuffer, exec.TrueCode);

				Buffer falseBuffer = new Buffer();
				SerializeLines(c, falseBuffer, exec.FalseCode);
				falseBuffer.Add(ByteCodes.JUMP(c, trueBuffer.Size));

				b.Add(ByteCodes.JUMP_IF_TRUE(c, falseBuffer.Size), exec.OpenParenToken);

				b.Add(falseBuffer);
				b.Add(trueBuffer);
			}
		}

		private static void SerializeLoop(Compiler c, Buffer b, Loop exec)
		{
			if (exec.IsForLoop)
			{
				SerializeForLoop(c, b, exec);
			}
			else if (exec.IsWhileLoop)
			{
				SerializeWhileLoop(c, b, exec);
			}
			else if (exec.IsDoWhileLoop)
			{
				SerializeDoWhileLoop(c, b, exec);
			}
			else
			{
				throw new Exception("Unrecognized loop");
			}
		}

		private static void SerializeDoWhileLoop(Compiler c, Buffer b, Loop loop)
		{
			/*
			 * do-while loop:
			 *   JUMP TARGET
			 *   CODE
			 *   CONDITION + JUMP IF TRUE    
			 */
			Buffer code = new Buffer();
			SerializeLines(c, code, loop.Code);

			ByteCodeExprSerializer.Serialize(c, code, loop.Condition);

			code.Add(ByteCodes.JUMP_IF_TRUE(c, -code.Size - 1), loop.ConditionOpenParenToken);

			code.ResolveBreaksAndContinues(c, true);

			b.Add(code);
		}

		private static void SerializeWhileLoop(Compiler c, Buffer b, Loop loop)
		{
			/*
			 * while loop:
			 *   JUMP TARGET
			 *   CONDITION + JUMP IF FALSE
			 *   CODE
			 *   JUMP UNCONDITIONALLY
			 *   BREAK JUMP TARGET
			 */
			Buffer code = new Buffer();
			SerializeLines(c, code, loop.Code);

			Buffer condition = new Buffer();
			ByteCodeExprSerializer.Serialize(c, condition, loop.Condition);
			condition.Add(ByteCodes.JUMP_IF_FALSE(c, code.Size + 1), loop.ConditionOpenParenToken); // +1 to skip unconditional jump-back that hasn't been serialized yet

			condition.Add(code);
			code = condition;

			code.Add(ByteCodes.JUMP(c, -code.Size - 1));

			code.ResolveBreaksAndContinues(c, true);

			b.Add(code);
		}

		private static void SerializeForLoop(Compiler c, Buffer b, Loop loop)
		{
			/*
			 * for loop:
			 *   INIT
			 *   JUMP TARGET
			 *   CONDITION + JUMP IF FALSE
			 *   CODE
			 *   STEP
			 *   JUMP UNCONDITIONALLY
			 *   BREAK JUMP TARGET
			 */
			Buffer code = new Buffer();
			SerializeLines(c, code, loop.Code);
			SerializeLines(c, code, loop.Step);

			Buffer condition = new Buffer();
			ByteCodeExprSerializer.Serialize(c, condition, loop.Condition);
			condition.Add(ByteCodes.JUMP_IF_FALSE(c, code.Size + 1 /* +1 for jump back */), loop.ConditionOpenParenToken);

			condition.Add(code);
			code = condition;

			code.Add(ByteCodes.JUMP(c, -code.Size - 1));

			code.ResolveBreaksAndContinues(c, true);

			SerializeLines(c, b, loop.Initialization);
			b.Add(code);
		}

		private static void SerializeBreak(Compiler c, Buffer b, Break exec)
		{
			b.AddBreak();
		}

		private static void SerializeSwitch(Compiler c, Buffer b, Switch exec)
		{
			List<Buffer> cases = new List<Buffer>();
			int offset = 0;
			List<int> switchParams = new List<int>();
			bool isString = false;

			isString = exec.Cases.Length > 0 && !exec.Cases[0].IsDefault && exec.Cases[0].Expression is StringConstant;

			switchParams.Add(isString ? 1 : 0);

			foreach (SwitchCase sc in exec.Cases)
			{
				// the ensurance for this being last is in the parser.
				if (sc.IsDefault)
				{
					switchParams.Add(0);
					switchParams.Add(offset);
				}
				else
				{
					Expression expr = sc.Expression;
					if (expr is StringConstant)
					{
						if (isString)
						{
							switchParams.Add(((StringConstant)expr).Index);
							switchParams.Add(offset);
						}
						else
						{
							throw new Exception("All switch cases must be of the same type.");
						}
					}
					else if (expr is NumericalConstant)
					{
						if (!isString)
						{
							switchParams.Add(((NumericalConstant)expr).Index);
							switchParams.Add(offset);
						}
						else
						{
							throw new Exception("All switch cases must be of the same type.");
						}
					}
				}
				Executable[] lines = sc.Code;
				Buffer caseBuffer = new Buffer();
				foreach (Executable line in lines)
				{
					Serialize(c, caseBuffer, line);
				}
				cases.Add(caseBuffer);
				offset += caseBuffer.Size;
			}

			b.Add(ByteCodes.SWITCH(c, switchParams.ToArray())); // only constant values are allowed in a switch, so there should be no need for token information
			foreach (Buffer caseBuffer in cases)
			{
				b.Add(caseBuffer);
			}

			b.ResolveBreaksAndContinues(c, false);
		}

		private static void SerializeExpressionAsExecutable(Compiler c, Buffer b, ExpressionAsExecutable exec)
		{
			// TODO: verify that only function calls are used here.
			ByteCodeExprSerializer.Serialize(c, b, exec.Expression);
			b.Add(ByteCodes.POP(c));
		}

		public static void SerializeReturn(Compiler c, Buffer b, Return item)
		{
			ByteCodeExprSerializer.Serialize(c, b, item.Value);
			b.Add(ByteCodes.RETURN(c));
		}
	}
}
